﻿/***************************************************************************

Copyright (c) Microsoft Corporation 2010.

This code is licensed using the Microsoft Public License (Ms-PL).  The text of the license
can be found here:

http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx

***************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using DocumentFormat.OpenXml.Packaging;

namespace OpenXmlPowerTools
{
    public class SimplifyMarkupSettings
    {
        public bool RemoveContentControls;
        public bool RemoveSmartTags;
        public bool RemoveRsidInfo;
        public bool RemoveComments;
        public bool RemoveEndAndFootNotes;
        public bool ReplaceTabsWithSpaces;
        public bool RemoveFieldCodes;
        public bool RemovePermissions;
        public bool RemoveProof;
        public bool RemoveSoftHyphens;
        public bool RemoveLastRenderedPageBreak;
    }

    public static class MarkupSimplifier
    {
        public class InternalException : Exception
        {
            public InternalException(string message) : base(message) { }
        }

        public class InvalidSettingsException : Exception
        {
            public InvalidSettingsException(string message) : base(message) { }
        }

        private static object RemoveCustomXmlAndContentControlsTransform(
            XNode node, SimplifyMarkupSettings simplifyMarkupSettings)
        {
            XElement element = node as XElement;
            if (element != null)
            {
                if (simplifyMarkupSettings.RemoveSmartTags &&
                    element.Name == W.smartTag)
                    return element
                        .Elements()
                        .Select(e =>
                            RemoveCustomXmlAndContentControlsTransform(e,
                                simplifyMarkupSettings));

                if (simplifyMarkupSettings.RemoveContentControls &&
                    element.Name == W.sdt)
                    return element
                        .Element(W.sdtContent)
                        .Elements()
                        .Select(e =>
                            RemoveCustomXmlAndContentControlsTransform(e,
                                simplifyMarkupSettings));
            }
            return node;
        }

        private static object RemoveRsidTransform(XNode node)
        {
            XElement element = node as XElement;
            if (element != null)
            {
                if (element.Name == W.rsid)
                    return null;
                return new XElement(element.Name,
                    element.Attributes().Where(a => a.Name != W.rsid &&
                        a.Name != W.rsidDel &&
                        a.Name != W.rsidP &&
                        a.Name != W.rsidR &&
                        a.Name != W.rsidRDefault &&
                        a.Name != W.rsidRPr &&
                        a.Name != W.rsidSect &&
                        a.Name != W.rsidTr),
                    element.Nodes().Select(n => RemoveRsidTransform(n)));
            }
            return node;
        }

        private static XAttribute GetXmlSpaceAttribute(
            string textElementValue)
        {
            if (textElementValue.Length > 0 &&
                (textElementValue[0] == ' ' ||
                textElementValue[textElementValue.Length - 1] == ' '))
                return new XAttribute(XNamespace.Xmlns + "space",
                    "preserve");
            return null;
        }

        private static object RemoveSuperfluousRunsTransform(XNode node)
        {
            XElement element = node as XElement;
            if (element != null)
            {
                if (element.Name == W.p)
                {
                    XElement paragraph = element;
                    var runGroups = paragraph
                        .Elements()
                        .GroupAdjacent(r =>
                        {
                            if (r.Name != W.r)
                                return "NotRuns";
                            XElement rPr = r.Element(W.rPr);
                            if (rPr == null)
                                return "NoRunProperties";
                            return rPr.ToString(
                                SaveOptions.DisableFormatting);
                        });
                    XElement newParagraph = new XElement(W.p,
                        paragraph.Attributes(),
                        runGroups.Select(g =>
                        {
                            if (g.Key == "NotRuns")
                                return (object)g;
                            if (g.Key == "NoRunProperties")
                            {
                                XElement newRun = new XElement(W.r,
                                    g.First().Attributes(),
                                    g.Elements()
                                        .GroupAdjacent(c => c.Name)
                                        .Select(gc =>
                                        {
                                            if (gc.Key != W.t)
                                                return (object)gc;
                                            string textElementValue =
                                                gc.Select(t => (string)t)
                                                  .StringConcatenate();
                                            return new XElement(W.t,
                                                GetXmlSpaceAttribute(
                                                    textElementValue),
                                                    textElementValue);
                                        }));
                                return newRun;
                            }
                            XElement runPropertyElement = XElement.Parse(
                                g.Key);
                            runPropertyElement.Attributes()
                                .Where(a => a.IsNamespaceDeclaration)
                                .Remove();
                            XElement newRunWithProperties = new XElement(
                                W.r,
                                g.First().Attributes(),
                                runPropertyElement,
                                g.Elements()
                                    .Where(e => e.Name != W.rPr)
                                    .GroupAdjacent(c => c.Name)
                                    .Select(gc =>
                                    {
                                        if (gc.Key != W.t)
                                            return (object)gc;
                                        string textElementValue = gc
                                            .Select(t => (string)t)
                                            .StringConcatenate();
                                        return new XElement(W.t,
                                            GetXmlSpaceAttribute(
                                                textElementValue),
                                                textElementValue);
                                    }));
                            return newRunWithProperties;
                        }
                        ));
                    return newParagraph;
                }
                return new XElement(element.Name,
                    element.Attributes(),
                    element.Nodes().Select(n =>
                        RemoveSuperfluousRunsTransform(n)));
            }
            return node;
        }

        private static object RemoveEmptyRunsAndRunPropertiesTransform(
            XNode node)
        {
            XElement element = node as XElement;
            if (element != null)
            {
                if ((element.Name == W.r || element.Name == W.rPr) &&
                    !element.Nodes().Any())
                    return null;
                return new XElement(element.Name,
                    element.Attributes(),
                    element.Nodes()
                        .Select(n =>
                            RemoveEmptyRunsAndRunPropertiesTransform(n)));
            }
            return node;
        }

        // lastRenderedPageBreak, permEnd, permStart, proofErr, noProof
        // softHyphen:
        // Remove when simplifying.

        // fldSimple, fldData, fldChar, instrText:
        // For hyperlinks, generate same in XHtml.  Other than hyperlinks, do the following:
        // - collapse fldSimple
        // - remove fldSimple, fldData, fldChar, instrText.

        private static object SimplifyMarkupTransform(
            WordprocessingDocument doc, XNode node,
            SimplifyMarkupSettings settings)
        {
            XElement element = node as XElement;
            if (element != null)
            {
                if (settings.RemovePermissions &&
                    (element.Name == W.permEnd ||
                    element.Name == W.permStart))
                    return null;

                if (settings.RemoveProof &&
                    (element.Name == W.proofErr ||
                    element.Name == W.noProof))
                    return null;

                if (settings.RemoveSoftHyphens &&
                    element.Name == W.softHyphen)
                    return null;

                if (settings.RemoveLastRenderedPageBreak &&
                    element.Name == W.lastRenderedPageBreak)
                    return null;

                if (settings.ReplaceTabsWithSpaces && element.Name == W.tab &&
                    element.Parent.Name == W.r)
                    return new XElement(W.t,
                        new XAttribute(XNamespace.Xml + "space", "preserve"),
                        " ");

                if (settings.RemoveComments &&
                    (element.Name == W.commentRangeStart ||
                    element.Name == W.commentRangeEnd ||
                    element.Name == W.commentReference ||
                    element.Name == W.annotationRef))
                    return null;

                if (settings.RemoveComments &&
                    element.Name == W.rStyle &&
                    element.Attribute(W.val).Value == "CommentReference")
                    return null;

                if (settings.RemoveEndAndFootNotes &&
                    (element.Name == W.endnoteReference ||
                    element.Name == W.footnoteReference))
                    return null;

                if (settings.RemoveFieldCodes)
                {
                    if (element.Name == W.fldSimple)
                        return element.Elements().Select(e =>
                            SimplifyMarkupTransform(doc, e, settings));
                    if (element.Name == W.fldData ||
                        element.Name == W.fldChar ||
                        element.Name == W.instrText)
                        return null;
                }

                return new XElement(element.Name,
                    element.Attributes(),
                    element.Nodes().Select(n =>
                        SimplifyMarkupTransform(doc, n, settings)));
            }
            return node;
        }

        public static void SimplifyMarkup(WordprocessingDocument doc,
            SimplifyMarkupSettings settings)
        {
            XDocument xdoc = doc.MainDocumentPart.GetXDocument();
            XElement newRoot = xdoc.Root;

            // Need to do this first to enable simplifying hyperlinks.
            if (settings.RemoveContentControls ||
                settings.RemoveSmartTags)
                newRoot = (XElement)
                    RemoveCustomXmlAndContentControlsTransform(
                        newRoot, settings);

            // This may touch many elements, so needs to be its own
            // transform.
            if (settings.RemoveRsidInfo)
                newRoot = (XElement)RemoveRsidTransform(newRoot);

            if (settings.RemoveComments ||
                settings.RemoveEndAndFootNotes ||
                settings.ReplaceTabsWithSpaces ||
                settings.RemoveFieldCodes ||
                settings.RemovePermissions ||
                settings.RemoveProof)
                newRoot = (XElement)SimplifyMarkupTransform(doc, newRoot,
                    settings);

            // Remove runs and run properties that have become empty due to previous
            // transforms.
            newRoot = (XElement)
                RemoveEmptyRunsAndRunPropertiesTransform(newRoot);

            // Remove adjacent runs that have identical run properties.
            newRoot = (XElement)RemoveSuperfluousRunsTransform(newRoot);

            // The last thing to do is to again remove runs and run properties
            // that have become empty due to previous transforms.
            newRoot = (XElement)
                RemoveEmptyRunsAndRunPropertiesTransform(newRoot);

            doc.MainDocumentPart.PutXDocument(new XDocument(newRoot));
        }
    }
}
